La funzione setsockopt (abbreviazione di "set socket options") รจ uno strumento fondamentale
nella programmazione di rete che permette di configurare e modificare il comportamento delle socket.
Quando lavoriamo con le socket in Python, spesso le impostazioni predefinite non sono sufficienti per
tutti gli scenari applicativi, e qui entra in gioco setsockopt, che ci permette di personalizzare
come le socket si comportano a livello di sistema operativo.
Questa funzione agisce come un'interfaccia diretta al kernel del sistema operativo, permettendoci di
modificare parametri interni che controllano aspetti come la riutilizzabilitร degli indirizzi,
la dimensione dei buffer, i timeout, la possibilitร di trasmettere in broadcast e molto altro.
ร particolarmente importante quando si sviluppano applicazioni di rete che richiedono configurazioni
specifiche o quando si lavora con protocolli come UDP per scenari avanzati come il broadcasting o
il multicasting.
๐ฏ Quando Usare setsockopt
Broadcasting UDP: Per inviare messaggi a tutti i dispositivi su una rete locale
Riavvio Rapido Server: Per evitare errori "Address already in use" quando si riavvia un server
Ottimizzazione Buffer: Per aumentare o diminuire la dimensione dei buffer di ricezione/trasmissione
Timeout Socket: Per configurare quanto tempo una socket deve attendere prima di timeout
Keep-Alive TCP: Per mantenere connessioni TCP attive e rilevare disconnessioni
Multicast: Per unirsi a gruppi multicast e ricevere traffico multicast
๐ 2. Sintassi e Parametri di setsockopt
La funzione setsockopt in Python ha una sintassi precisa che richiede la comprensione di tre parametri
fondamentali. Ogni parametro ha un ruolo specifico nel determinare quale opzione di socket vogliamo
modificare e quale valore vogliamo assegnarle.
socket_oggetto.setsockopt(level, optname, value)
๐ Parametri Dettagliati
1๏ธโฃ level (Livello di Protocollo)
Il parametro level specifica a quale livello dello stack di rete si applica l'opzione.
Questo รจ importante perchรฉ le reti operano su diversi livelli (modello OSI), e alcune opzioni sono
specifiche per certi livelli. I valori piรน comuni sono:
Costante
Valore
Descrizione
socket.SOL_SOCKET
1
Opzioni a livello di socket generiche, indipendenti dal protocollo. Questo รจ il livello piรน comune da utilizzare per opzioni che si applicano a tutte le socket.
socket.IPPROTO_TCP
6
Opzioni specifiche per il protocollo TCP. Usato per configurare comportamenti specifici delle connessioni TCP come TCP_NODELAY.
socket.IPPROTO_IP
0
Opzioni specifiche per il protocollo IP. Usato principalmente per configurazioni multicast e routing.
socket.IPPROTO_UDP
17
Opzioni specifiche per il protocollo UDP. Usato raramente perchรฉ UDP ha poche opzioni configurabili.
โ ๏ธ Attenzione
ร fondamentale usare il livello corretto per l'opzione che si vuole impostare. Ad esempio,
l'opzione SO_BROADCAST deve essere impostata a livello SOL_SOCKET, mentre TCP_NODELAY richiede
IPPROTO_TCP. Usare il livello sbagliato produrrร un errore di sistema operativo.
2๏ธโฃ optname (Nome dell'Opzione)
Il parametro optname specifica esattamente quale opzione della socket vogliamo modificare.
Python fornisce costanti predefinite nel modulo socket per rappresentare queste opzioni. Ogni costante
corrisponde a un'opzione specifica supportata dal sistema operativo.
Costante
Descrizione Dettagliata
Tipo Valore
SO_BROADCAST
Abilita la trasmissione di pacchetti broadcast sulla socket. Fondamentale per UDP quando si vuole inviare messaggi all'indirizzo broadcast (255.255.255.255) che raggiunge tutti i dispositivi nella rete locale.
int (0 o 1)
SO_REUSEADDR
Permette di riutilizzare immediatamente un indirizzo locale anche se รจ ancora in stato TIME_WAIT. Estremamente utile per server che si riavviano frequentemente, evitando l'errore "Address already in use".
int (0 o 1)
SO_REUSEPORT
Permette a piรน socket di bind sullo stesso indirizzo e porta. Utile per bilanciamento del carico e applicazioni multi-processo che ascoltano sulla stessa porta.
int (0 o 1)
SO_KEEPALIVE
Abilita l'invio periodico di messaggi keep-alive su connessioni TCP inattive. Permette di rilevare se la connessione รจ ancora attiva o se l'altro endpoint รจ disconnesso.
int (0 o 1)
SO_RCVBUF
Imposta la dimensione del buffer di ricezione della socket. Un buffer piรน grande puรฒ gestire burst di traffico in arrivo, ma consuma piรน memoria.
int (bytes)
SO_SNDBUF
Imposta la dimensione del buffer di trasmissione della socket. Influenza quanti dati possono essere accodati per l'invio prima che send() blocchi.
int (bytes)
SO_RCVTIMEO
Imposta il timeout per le operazioni di ricezione. Se non arrivano dati entro questo tempo, recv() solleva un'eccezione di timeout.
struct (seconds, microseconds)
SO_SNDTIMEO
Imposta il timeout per le operazioni di invio. Se i dati non possono essere inviati entro questo tempo, send() solleva un'eccezione.
struct (seconds, microseconds)
SO_LINGER
Controlla il comportamento di close() quando ci sono dati ancora in attesa di essere inviati. Puรฒ forzare la chiusura immediata o attendere.
struct (l_onoff, l_linger)
TCP_NODELAY
Disabilita l'algoritmo di Nagle in TCP, che normalmente accumula piccoli pacchetti. Utile per applicazioni real-time dove la latenza รจ critica.
int (0 o 1)
3๏ธโฃ value (Valore dell'Opzione)
Il parametro value rappresenta il valore che vogliamo assegnare all'opzione.
Il tipo di questo valore dipende dall'opzione che stiamo configurando:
Valori Booleani (0 o 1): La maggior parte delle opzioni on/off come SO_BROADCAST, SO_REUSEADDR accettano 0 (disabilitato) o 1 (abilitato). In Python, possiamo anche usare True/False che vengono convertiti automaticamente.
Valori Interi: Opzioni come SO_RCVBUF e SO_SNDBUF richiedono un intero che rappresenta la dimensione in bytes del buffer.
Strutture: Alcune opzioni come SO_LINGER richiedono una struttura dati piรน complessa, che puรฒ essere creata usando il modulo struct di Python.
Il broadcasting รจ una tecnica di comunicazione di rete dove un singolo messaggio viene inviato
simultaneamente a tutti i dispositivi presenti in una rete locale. Questo รจ particolarmente utile in
UDP per scenari come il service discovery, dove un client deve trovare tutti i server disponibili sulla
rete senza conoscere i loro indirizzi IP specifici.
Per inviare messaggi broadcast in UDP, รจ necessario abilitare esplicitamente l'opzione SO_BROADCAST
sulla socket. Questo รจ un meccanismo di sicurezza implementato dal sistema operativo: per default,
le socket non possono inviare broadcast per prevenire che programmi malevoli o mal configurati
inondino la rete con traffico non desiderato.
๐ Come Funziona il Broadcasting
CLIENT 192.168.1.100
โ
BROADCAST 255.255.255.255
โ
SERVER 1 192.168.1.10
SERVER 2 192.168.1.20
SERVER 3 192.168.1.30
๐ง Implementazione Completa: Broadcast UDP
๐ค Client Broadcast
Il client broadcast รจ responsabile dell'invio di messaggi all'indirizzo broadcast della rete.
L'indirizzo 255.255.255.255 รจ l'indirizzo broadcast universale che raggiunge tutti i dispositivi
sulla rete locale. Vediamo un'implementazione dettagliata passo per passo.
๐ client_broadcast.py - Versione Completa
import socket
import time
# Configurazione
BROADCAST_IP = "255.255.255.255"# Indirizzo broadcast universale
BROADCAST_PORT = 9999# Porta su cui i server ascoltano
MESSAGE = "DISCOVER_SERVER"# Messaggio di discovery# STEP 1: Creazione della socket UDPprint("๐จ Creazione socket UDP...")
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
# STEP 2: FONDAMENTALE - Abilita broadcasting sulla socket# Senza questa riga, sendto() fallirร con "Permission denied"print("๐ก Abilitazione broadcast...")
client_socket.setsockopt(
socket.SOL_SOCKET, # Livello: socket generico
socket.SO_BROADCAST, # Opzione: abilita broadcast1# Valore: 1 = abilitato
)
print("โ Broadcast abilitato con successo!")
# STEP 3: Imposta un timeout per la ricezione delle risposte# Questo evita che il programma si blocchi indefinitamente
client_socket.settimeout(3.0) # 3 secondi di timeout# STEP 4: Invia il messaggio broadcastprint(f"\n๐ข Invio messaggio broadcast: '{MESSAGE}'")
print(f"๐ฏ Destinazione: {BROADCAST_IP}:{BROADCAST_PORT}")
# sendto() invia il messaggio all'indirizzo broadcast# Tutti i dispositivi sulla rete locale riceveranno questo messaggio
bytes_sent = client_socket.sendto(
MESSAGE.encode('utf-8'),
(BROADCAST_IP, BROADCAST_PORT)
)
print(f"โ Inviati {bytes_sent} bytes in broadcast")
# STEP 5: Raccolta delle risposte dai serverprint("\n๐ In ascolto per risposte dai server...")
print("โณ Timeout: 3 secondi\n")
server_trovati = 0
indirizzi_server = []
# Loop per ricevere multiple rispostewhileTrue:
try:
# Riceve risposta da un server
data, server_address = client_socket.recvfrom(1024)
risposta = data.decode('utf-8')
# Evita duplicati (alcuni server potrebbero rispondere piรน volte)if server_address not in indirizzi_server:
server_trovati += 1
indirizzi_server.append(server_address)
print(f"๐ Server #{server_trovati} trovato!")
print(f" ๐ Indirizzo: {server_address[0]}:{server_address[1]}")
print(f" ๐ฌ Risposta: {risposta}\n")
except socket.timeout:
# Timeout raggiunto - nessun altro server ha rispostobreak# STEP 6: Riepilogo finaleprint(f"\n๐ Riepilogo Discovery:")
print(f" Server trovati: {server_trovati}")
if server_trovati == 0:
print(" โ ๏ธ Nessun server ha risposto al broadcast")
else:
print(" ๐ Lista indirizzi server:")
for idx, addr inenumerate(indirizzi_server, 1):
print(f" {idx}. {addr[0]}:{addr[1]}")
except PermissionError:
# Errore comune: broadcast non abilitato o problemi di permessiprint("โ ERRORE: Permesso negato per l'invio broadcast")
print(" Verifica che SO_BROADCAST sia abilitato!")
exceptExceptionas e:
print(f"โ Errore: {e}")
finally:
# STEP 7: Chiusura della socket
client_socket.close()
print("\n๐ Socket chiusa. Programma terminato.")
๐ฅ Server Broadcast
Il server broadcast ascolta sulla porta broadcast e risponde a qualsiasi messaggio di discovery ricevuto.
Nota che il server NON ha bisogno di abilitare SO_BROADCAST per ricevere messaggi broadcast, solo per inviarli.
๐ server_broadcast.py - Versione Completa
import socket
import platform
# Configurazione
SERVER_PORT = 9999# Porta su cui ascoltare
BUFFER_SIZE = 1024# Dimensione buffer ricezione# STEP 1: Creazione socket UDPprint("๐จ Creazione socket UDP...")
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
# STEP 2: Opzionale ma consigliato - Abilita SO_REUSEADDR# Permette di riavviare il server senza attendere
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
print("โ SO_REUSEADDR abilitato")
# STEP 3: Bind su tutti gli indirizzi IP disponibili# "0.0.0.0" significa "ascolta su tutte le interfacce di rete"# Questo permette di ricevere broadcast su qualsiasi interfaccia
server_socket.bind(("0.0.0.0", SERVER_PORT))
print(f"๐ก Server in ascolto su 0.0.0.0:{SERVER_PORT}")
print("๐ Pronto a ricevere messaggi broadcast...\n")
# STEP 4: Loop principale di ricezione
contatore_messaggi = 0whileTrue:
# Attende ricezione di un datagramma
data, client_address = server_socket.recvfrom(BUFFER_SIZE)
contatore_messaggi += 1# Decodifica il messaggio ricevuto
messaggio = data.decode('utf-8')
print(f"๐จ Messaggio #{contatore_messaggi} ricevuto")
print(f" ๐น Da: {client_address[0]}:{client_address[1]}")
print(f" ๐น Contenuto: '{messaggio}'")
# STEP 5: Prepara e invia rispostaif messaggio == "DISCOVER_SERVER":
# Crea una risposta informativa con dettagli del server
hostname = platform.node()
risposta = f"SERVER_FOUND|{hostname}"# Invia risposta direttamente al client (non in broadcast)
server_socket.sendto(
risposta.encode('utf-8'),
client_address
)
print(f" โ Risposta inviata: '{risposta}'\n")
else:
# Messaggio non riconosciutoprint(" โ ๏ธ Messaggio non riconosciuto - ignorato\n")
except KeyboardInterrupt:
print("\n\n๐ Server interrotto dall'utente")
print(f"๐ Statistiche: {contatore_messaggi} messaggi elaborati")
exceptExceptionas e:
print(f"โ Errore: {e}")
finally:
# STEP 6: Chiusura pulita della socket
server_socket.close()
print("๐ Socket chiusa. Server terminato.")
โ ๏ธ Errori Comuni nel Broadcasting
Permission Denied: Significa che SO_BROADCAST non รจ stato abilitato sulla socket prima di inviare al broadcast address
Network Unreachable: Il firewall potrebbe bloccare il traffico broadcast. Verifica le impostazioni del firewall
Nessuna Risposta: I server potrebbero non essere sulla stessa rete locale, o potrebbero avere firewall che bloccano UDP
Bind Error sul Server: Un altro processo sta giร usando la porta. Usa SO_REUSEADDR o cambia porta
๐งช Test del Broadcasting
๐ป Output Atteso
Terminal Server:
๐จ Creazione socket UDP...
โ SO_REUSEADDR abilitato
๐ก Server in ascolto su 0.0.0.0:9999
๐ Pronto a ricevere messaggi broadcast...
๐จ Messaggio #1 ricevuto
๐น Da: 192.168.1.100:54321
๐น Contenuto: 'DISCOVER_SERVER'
โ Risposta inviata: 'SERVER_FOUND|MyComputer'
Terminal Client:
๐จ Creazione socket UDP...
๐ก Abilitazione broadcast...
โ Broadcast abilitato con successo!
๐ข Invio messaggio broadcast: 'DISCOVER_SERVER'
๐ฏ Destinazione: 255.255.255.255:9999
โ Inviati 15 bytes in broadcast
๐ In ascolto per risposte dai server...
โณ Timeout: 3 secondi
๐ Server #1 trovato!
๐ Indirizzo: 192.168.1.10:9999
๐ฌ Risposta: SERVER_FOUND|ServerA
๐ Server #2 trovato!
๐ Indirizzo: 192.168.1.20:9999
๐ฌ Risposta: SERVER_FOUND|ServerB
๐ Riepilogo Discovery:
Server trovati: 2
๐ Lista indirizzi server:
1. 192.168.1.10:9999
2. 192.168.1.20:9999
๐ Socket chiusa. Programma terminato.
๐ 4. SO_REUSEADDR - Riutilizzo Indirizzi
Una delle opzioni piรน utilizzate nella programmazione di rete รจ SO_REUSEADDR.
Questa opzione risolve un problema molto comune che si verifica quando si sviluppano e testano
applicazioni server: l'errore "Address already in use" (Indirizzo giร in uso).
Quando una socket TCP viene chiusa, il sistema operativo mantiene la connessione in uno stato chiamato
TIME_WAIT per un certo periodo di tempo (tipicamente 30-120 secondi). Questo serve a
garantire che tutti i pacchetti ritardati della connessione precedente siano stati ricevuti e non
interferiscano con nuove connessioni. Durante questo periodo, non รจ possibile riutilizzare l'indirizzo
e la porta, causando l'errore quando si cerca di riavviare il server.
โฑ๏ธ Timeline del Problema TIME_WAIT
00:00 - โ Server avviato su 127.0.0.1:5000
00:30 - ๐ Server fermato (CTRL+C)
00:31 - โ Tentativo riavvio: "Address already in use"
00:32 - โณ Socket in stato TIME_WAIT...
00:45 - โณ Socket ancora in TIME_WAIT...
01:00 - โณ Socket ancora in TIME_WAIT...
02:30 - โ Finalmente possibile riavviare!
Con SO_REUSEADDR, il riavvio รจ immediato! โก
๐ Implementazione Corretta
โ Server TCP con SO_REUSEADDR
import socket
# Creazione socket TCP
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# FONDAMENTALE: Abilita SO_REUSEADDR PRIMA del bind()# Questo DEVE essere fatto prima di chiamare bind(), altrimenti non funziona
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Ora possiamo fare bind anche se l'indirizzo รจ in TIME_WAIT
server_socket.bind(("127.0.0.1", 5000))
server_socket.listen(5)
print("โ Server avviato con successo!")
print("๐ Riavvio rapido abilitato con SO_REUSEADDR")
try:
whileTrue:
client, addr = server_socket.accept()
print(f"๐ Connessione da {addr}")
# ... gestione client ...
client.close()
except KeyboardInterrupt:
print("\n๐ Server fermato")
finally:
server_socket.close()
โ Senza SO_REUSEADDR
Errore al riavvio rapido
Attesa di 30-120 secondi
Frustrante durante sviluppo
Richiede cambio porta manuale
โ Con SO_REUSEADDR
Riavvio immediato possibile
Nessuna attesa necessaria
Workflow di sviluppo fluido
Stessa porta sempre disponibile
โ ๏ธ Ordine delle Operazioni
ร FONDAMENTALE chiamare setsockopt(SO_REUSEADDR)PRIMA di
chiamare bind(). Se lo fai dopo, l'opzione non avrร effetto e continuerai
a ricevere l'errore "Address already in use".
I buffer di rete sono aree di memoria utilizzate dal sistema operativo per memorizzare temporaneamente
i dati in arrivo (buffer di ricezione) e i dati in uscita (buffer di trasmissione). Le dimensioni di
questi buffer influenzano significativamente le prestazioni e l'affidabilitร delle comunicazioni di rete,
specialmente in scenari ad alto traffico.
Il buffer di ricezione (SO_RCVBUF) memorizza i pacchetti in arrivo prima che
l'applicazione li legga con recv() o recvfrom(). Se i dati arrivano piรน velocemente di quanto
l'applicazione possa elaborarli, e il buffer si riempie, i nuovi pacchetti vengono scartati.
Questo รจ particolarmente problematico in UDP dove non c'รจ ritrasmissione automatica.
Il buffer di trasmissione (SO_SNDBUF) memorizza i dati che l'applicazione vuole
inviare ma che il sistema operativo non ha ancora trasmesso sulla rete. Un buffer piรน grande permette
all'applicazione di continuare a scrivere dati anche quando la rete รจ temporaneamente lenta.
๐ Quando Modificare le Dimensioni dei Buffer
๐ Aumentare i Buffer
Quando:
๐น Streaming video/audio ad alta qualitร
๐ก Ricezione di burst di traffico UDP
๐พ Trasferimento di file di grandi dimensioni
๐ฎ Gaming online con alta frequenza di aggiornamenti
๐ Applicazioni di logging che ricevono molti messaggi
Benefici:
โ Riduzione della perdita di pacchetti UDP
โ Gestione migliore di traffico a burst
โ Throughput complessivo aumentato
๐ Diminuire i Buffer
Quando:
๐ฎ Applicazioni real-time dove la latenza รจ critica
๐ป Dispositivi con memoria limitata (IoT)
โก Protocolli che richiedono feedback immediato
Benefici:
โ Latenza ridotta (meno dati accodati)
โ Feedback piรน immediato sulla congestione
โ Consumo di memoria ridotto
๐ป Esempi Pratici di Configurazione Buffer
๐น Server di Streaming Video - Buffer Grandi
import socket
# Scenario: Server di streaming video ad alta definizione# Necessitร : Gestire burst di traffico senza perdere frames
streaming_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Configurazione buffer ottimizzata per streaming
BUFFER_SIZE = 2 * 1024 * 1024# 2 MB# Aumenta buffer di ricezione# Questo permette di accumulare piรน frames prima dell'elaborazione
streaming_socket.setsockopt(
socket.SOL_SOCKET,
socket.SO_RCVBUF,
BUFFER_SIZE
)
# Aumenta buffer di trasmissione# Questo permette di accodare piรน frames da inviare
streaming_socket.setsockopt(
socket.SOL_SOCKET,
socket.SO_SNDBUF,
BUFFER_SIZE
)
# Verifica le dimensioni effettive impostate# Nota: il sistema operativo potrebbe limitare la dimensione massima
actual_rcv = streaming_socket.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)
actual_snd = streaming_socket.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
print(f"๐ Buffer Configuration:")
print(f" ๐ฅ Receive Buffer: {actual_rcv / 1024 / 1024:.2f} MB")
print(f" ๐ค Send Buffer: {actual_snd / 1024 / 1024:.2f} MB")
streaming_socket.bind(("0.0.0.0", 8888))
print("๐น Streaming server ready with optimized buffers!")
๐ฎ Gaming Client Real-Time - Buffer Piccoli
import socket
# Scenario: Client di gaming online multiplayer# Necessitร : Latenza minima, dati vecchi sono inutili
game_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Configurazione buffer ottimizzata per low-latency
SMALL_BUFFER = 8192# 8 KB - molto piccolo intenzionalmente# Buffer piccoli per minimizzare la latenza# Dati vecchi accodati non sono utili in un gioco real-time
game_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, SMALL_BUFFER)
game_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, SMALL_BUFFER)
print("๐ฎ Gaming client configured for minimal latency")
print(f"โก Buffer size: {SMALL_BUFFER} bytes")
print(" (Small buffers = fresh data = low latency)")
๐ Server di Logging - Buffer Asimmetrici
import socket
# Scenario: Server che riceve molti log ma invia poche risposte# Necessitร : Grande buffer ricezione, piccolo buffer invio
log_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Buffer di ricezione grande per gestire burst di log
log_socket.setsockopt(
socket.SOL_SOCKET,
socket.SO_RCVBUF,
1024 * 1024# 1 MB
)
# Buffer di invio piccolo perchรฉ inviamo poche risposte
log_socket.setsockopt(
socket.SOL_SOCKET,
socket.SO_SNDBUF,
16384# 16 KB
)
log_socket.bind(("0.0.0.0", 5140)) # Porta syslog standardprint("๐ Log server ready with asymmetric buffers")
print(" High-volume receive, low-volume send")
๐ก Nota Importante: Il sistema operativo puรฒ limitare la dimensione massima dei buffer.
Su Linux, puoi verificare i limiti con:
Se necessiti di buffer piรน grandi, dovrai modificare questi parametri di sistema (richiede privilegi di amministratore).
โฑ๏ธ 6. SO_RCVTIMEO e SO_SNDTIMEO - Gestione Timeout
I timeout sono meccanismi cruciali per evitare che un'applicazione rimanga bloccata indefinitamente
in attesa di operazioni di rete che potrebbero non completarsi mai. Senza timeout, una chiamata a
recv() o send() potrebbe bloccare il programma per sempre se la rete รจ disconnessa o se l'altro
endpoint non risponde.
SO_RCVTIMEO e SO_SNDTIMEO permettono di impostare timeout a livello di socket usando setsockopt,
offrendo un'alternativa al metodo settimeout() che molti programmatori conoscono. La differenza
principale รจ che SO_RCVTIMEO/SO_SNDTIMEO offrono maggiore flessibilitร e controllo granulare.
๐ Confronto: settimeout() vs SO_RCVTIMEO/SO_SNDTIMEO
Caratteristica
settimeout()
SO_RCVTIMEO/SO_SNDTIMEO
Facilitร d'uso
โ Molto semplice
โ ๏ธ Piรน complesso (richiede struct)
Granularitร
โ Stesso timeout per recv e send
โ Timeout separati per recv e send
Portabilitร
โ Funziona ovunque
โ ๏ธ Dipende dal sistema operativo
Precisione
โ Millisecondi
โ Microsecondi
Controllo
โ Limitato
โ Massimo controllo
โฐ Esempio: Timeout con settimeout() - Metodo Semplice
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Metodo semplice: un solo timeout per tutte le operazioni
client.settimeout(5.0) # 5 seconditry:
client.sendto(b"DATA", ("server.com", 9999))
data, addr = client.recvfrom(1024) # Timeout dopo 5 secondiprint(f"โ Ricevuto: {data}")
except socket.timeout:
print("โฐ Timeout! Nessuna risposta dal server")
โ๏ธ Esempio: Timeout con SO_RCVTIMEO - Metodo Avanzato
import socket
import struct
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Metodo avanzato: timeout granulare usando struct# struct.pack crea una struttura binaria con:# 'LL' = due long integers (su piattaforme a 64-bit)# Prima valore = secondi, secondo valore = microsecondi
timeout_recv = struct.pack('LL', 3, 500000) # 3.5 secondi (3s + 500000ฮผs)
timeout_send = struct.pack('LL', 1, 0) # 1.0 secondo# Imposta timeout diversi per ricezione e invio
client.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, timeout_recv)
client.setsockopt(socket.SOL_SOCKET, socket.SO_SNDTIMEO, timeout_send)
try:
# send() ha timeout di 1 secondo
client.sendto(b"DATA", ("server.com", 9999))
# recv() ha timeout di 3.5 secondi
data, addr = client.recvfrom(1024)
print(f"โ Ricevuto: {data}")
except socket.timeout:
print("โฐ Timeout! Operazione non completata in tempo")
โ ๏ธ Attenzione alla Portabilitร
Il formato della struttura per SO_RCVTIMEO/SO_SNDTIMEO varia tra sistemi operativi:
Linux/Unix (64-bit):struct.pack('LL', sec, usec)
Windows: Usa solo millisecondi come intero, non supporta struct
macOS: Come Linux ma potrebbe variare
Per applicazioni portabili, รจ consigliato usare settimeout() che funziona
uniformemente su tutti i sistemi operativi.
๐ฏ Caso d'Uso: Client con Retry e Timeout Progressivo
๐ Client UDP con Retry Intelligente
import socket
import time
defsend_with_retry(message, server_addr, max_retries=3):
"""
Invia un messaggio UDP con retry e timeout progressivo.
Strategia:
- Tentativo 1: timeout 1s
- Tentativo 2: timeout 2s
- Tentativo 3: timeout 4s
Questo approccio รจ utile quando la rete potrebbe essere lenta
ma non vogliamo attendere troppo ai primi tentativi.
"""
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for attempt inrange(1, max_retries + 1):
# Timeout progressivo: raddoppia ad ogni tentativo
timeout = attempt # 1s, 2s, 3s
client.settimeout(timeout)
print(f"\n๐ Tentativo {attempt}/{max_retries}")
print(f"โฐ Timeout: {timeout}s")
try:
# Invia messaggio
start_time = time.time()
client.sendto(message.encode(), server_addr)
print("๐ค Messaggio inviato")
# Attende risposta
data, addr = client.recvfrom(1024)
elapsed = time.time() - start_time
print(f"โ Risposta ricevuta in {elapsed:.3f}s")
print(f"๐ฌ Contenuto: {data.decode()}")
client.close()
return data.decode()
except socket.timeout:
print(f"โฐ Timeout dopo {timeout}s")
if attempt == max_retries:
print(f"\nโ Fallito dopo {max_retries} tentativi")
client.close()
returnNoneelse:
print("๐ Nuovo tentativo...")
exceptExceptionas e:
print(f"โ Errore: {e}")
client.close()
returnNone# Test della funzioneif __name__ == "__main__":
response = send_with_retry(
"PING",
("192.168.1.100", 9999),
max_retries=3
)
if response:
print(f"\n๐ Comunicazione riuscita!")
else:
print(f"\n๐ Impossibile contattare il server")
SO_KEEPALIVE รจ un'opzione TCP che permette di rilevare se una connessione รจ ancora attiva quando
non c'รจ traffico. Questo รจ utile per server che hanno connessioni persistenti con i client e vogliono
sapere se il client si รจ disconnesso silenziosamente (ad esempio, spegnimento improvviso o perdita
di connettivitร di rete).
๐ Server TCP con Keep-Alive
import socket
# Server TCP con keep-alive per rilevare disconnessioni
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Abilita keep-alive
server.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# Su Linux, puoi configurare parametri keep-alive specifici:try:
# TCP_KEEPIDLE: secondi di inattivitร prima del primo probe
server.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60)
# TCP_KEEPINTVL: intervallo tra i probe successivi
server.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10)
# TCP_KEEPCNT: numero di probe prima di dichiarare connessione morta
server.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5)
print("โ Keep-alive configurato:")
print(" ๐ Primo check dopo 60s di inattivitร ")
print(" ๐ Probe ogni 10s")
print(" ๐ Disconnessione dopo 5 probe falliti")
except AttributeError:
# Parametri TCP_KEEP* non disponibili su questo sistemaprint("โ ๏ธ Keep-alive abilitato con parametri di default del sistema")
server.bind(("0.0.0.0", 5000))
server.listen(5)
print("๐ Server con keep-alive attivo")
โก TCP_NODELAY - Disabilita l'Algoritmo di Nagle
L'algoritmo di Nagle รจ un meccanismo di ottimizzazione in TCP che raggruppa piccoli pacchetti per
ridurre l'overhead di rete. Tuttavia, questo introduce latenza perchรฉ TCP attende di accumulare
abbastanza dati prima di inviare. Per applicazioni real-time, questa latenza รจ inaccettabile.
๐ฎ Client Gaming con TCP_NODELAY
import socket
# Client per gaming online dove la latenza รจ critica
game_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Disabilita l'algoritmo di Nagle per zero latency
game_client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
print("โก TCP_NODELAY abilitato")
print(" I pacchetti vengono inviati immediatamente")
print(" Perfetto per: gaming, VoIP, applicazioni real-time")
game_client.connect(("game-server.com", 7777))
# Ogni send() viene trasmesso immediatamente, senza attesa
game_client.send(b"PLAYER_MOVE") # Inviato SUBITO
game_client.send(b"SHOOT") # Inviato SUBITO
๐ Con Algoritmo di Nagle (Default)
Piccoli pacchetti vengono accumulati
Invio quando buffer pieno o timeout
Riduce overhead di rete
Aumenta latenza
Usare per: trasferimento file
โก Con TCP_NODELAY
Ogni send() invia immediatamente
Nessuna accumulazione
Aumenta overhead di rete
Minimizza latenza
Usare per: gaming, VoIP
๐ SO_LINGER - Controllo Chiusura Socket
SO_LINGER controlla cosa succede quando chiudi una socket TCP che ha ancora dati da trasmettere.
Normalmente, close() ritorna immediatamente ma il sistema operativo continua a trasmettere i dati
in background. Con SO_LINGER, puoi forzare comportamenti diversi.
๐ Configurazione SO_LINGER
import socket
import struct
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# SO_LINGER richiede una struttura con due valori:# l_onoff: 0 = disabilitato, 1 = abilitato# l_linger: secondi di attesa (se l_onoff = 1)# OPZIONE 1: Chiusura normale (default)# close() ritorna subito, dati trasmessi in background
linger_off = struct.pack('ii', 0, 0)
tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, linger_off)
# OPZIONE 2: Attesa trasmissione dati# close() attende fino a 10 secondi per trasmettere tutto
linger_wait = struct.pack('ii', 1, 10) # attendi max 10 secondi
tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, linger_wait)
# OPZIONE 3: Chiusura brutale (RST invece di FIN)# close() scarta tutti i dati pendenti e chiude brutalmente
linger_abort = struct.pack('ii', 1, 0) # timeout = 0
tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, linger_abort)
๐ 8. Riepilogo e Best Practices
โ Checklist: Quando Usare setsockopt
Scenario
Opzione
Codice
๐ Riavvio rapido server
SO_REUSEADDR
setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
๐ก Broadcasting UDP
SO_BROADCAST
setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
๐น Streaming video
SO_RCVBUF/SO_SNDBUF
setsockopt(SOL_SOCKET, SO_RCVBUF, 2MB)
๐ฎ Gaming real-time
TCP_NODELAY
setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
๐ Rilevare disconnessioni
SO_KEEPALIVE
setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1)
โฐ Prevenire blocchi
SO_RCVTIMEO
setsockopt(SOL_SOCKET, SO_RCVTIMEO, ...)
๐ฏ Best Practices
Ordine delle Operazioni: Chiama sempre setsockopt() PRIMA di bind() per le opzioni che influenzano il binding (come SO_REUSEADDR)
Gestione Errori: Wrappa sempre le chiamate a setsockopt() in try-except perchรฉ alcune opzioni potrebbero non essere supportate su tutti i sistemi
Verifica Effettiva: Usa getsockopt() per verificare che l'opzione sia stata impostata correttamente
Portabilitร : Testa il codice su diversi sistemi operativi quando usi opzioni avanzate
Documentazione: Commenta sempre perchรฉ stai usando una particolare opzione socket
Buffer Size: Non esagerare con le dimensioni dei buffer - piรน grande non รจ sempre meglio
Timeout: Imposta sempre timeout appropriati per evitare blocchi indefiniti
โ ๏ธ Errori Comuni da Evitare
โ Dimenticare SO_BROADCAST: Senza abilitarlo, l'invio all'indirizzo broadcast fallirร con "Permission denied"
โ Impostare opzioni dopo bind(): Alcune opzioni come SO_REUSEADDR devono essere impostate prima del bind()
โ Ignorare le eccezioni: Non tutte le opzioni sono disponibili su tutti i sistemi operativi
โ Buffer troppo grandi: Possono consumare troppa memoria e non sempre migliorano le prestazioni
โ Non testare timeout: Timeout mal configurati possono causare problemi difficili da debuggare
โ Usare valori hardcoded: Usa costanti del modulo socket invece di valori numerici diretti
๐ฌ 9. Esempio Completo: Server Discovery Service
Per concludere questa lezione, vediamo un esempio completo che utilizza diverse opzioni di setsockopt
in un'applicazione reale: un sistema di service discovery dove i client possono trovare automaticamente
i server disponibili sulla rete locale usando broadcasting UDP.
๐ Sistema Completo di Service Discovery
๐ก Server Discovery (server_discovery.py)
import socket
import json
import platform
import threading
import time
from datetime import datetime
classDiscoveryServer:
def__init__(self, service_name, service_port, discovery_port=9999):
self.service_name = service_name
self.service_port = service_port
self.discovery_port = discovery_port
self.hostname = platform.node()
self.running = False# Statistiche
self.queries_received = 0
self.start_time = Nonedefstart(self):
"""Avvia il server discovery"""# Crea socket UDP
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Abilita riutilizzo porta (importante per riavvii rapidi)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Aumenta buffer di ricezione per gestire burst di discovery
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 65536)
# Bind su tutte le interfacce
self.socket.bind(("0.0.0.0", self.discovery_port))
self.running = True
self.start_time = datetime.now()
print(f"๐ Discovery Server avviato")
print(f" ๐ Servizio: {self.service_name}")
print(f" ๐ฅ๏ธ Hostname: {self.hostname}")
print(f" ๐ Porta servizio: {self.service_port}")
print(f" ๐ก Porta discovery: {self.discovery_port}")
print(f" ๐ In ascolto...\n")
# Thread per statistiche
stats_thread = threading.Thread(target=self._print_stats, daemon=True)
stats_thread.start()
# Loop principalewhile self.running:
try:
data, client_addr = self.socket.recvfrom(1024)
message = data.decode('utf-8')
if message == "DISCOVER_SERVICE":
self.queries_received += 1
self._handle_discovery(client_addr)
except KeyboardInterrupt:
breakexceptExceptionas e:
print(f"โ Errore: {e}")
self.stop()
def_handle_discovery(self, client_addr):
"""Gestisce una richiesta di discovery"""# Prepara risposta con dettagli del servizio
response_data = {
"service_name": self.service_name,
"hostname": self.hostname,
"service_port": self.service_port,
"timestamp": datetime.now().isoformat(),
"uptime_seconds": (datetime.now() - self.start_time).total_seconds()
}
response_json = json.dumps(response_data)
self.socket.sendto(response_json.encode('utf-8'), client_addr)
print(f"๐จ Discovery da {client_addr[0]}:{client_addr[1]}")
def_print_stats(self):
"""Stampa statistiche periodicamente"""while self.running:
time.sleep(30)
uptime = (datetime.now() - self.start_time).total_seconds()
print(f"\n๐ Statistiche (uptime: {uptime:.0f}s)")
print(f" Query ricevute: {self.queries_received}")
print(f" Media: {self.queries_received / (uptime / 60):.2f} query/min\n")
defstop(self):
"""Ferma il server"""
self.running = False
self.socket.close()
print("\n๐ Discovery Server fermato")
if __name__ == "__main__":
# Esempio: Server web che si registra per discovery
server = DiscoveryServer(
service_name="WebServer",
service_port=8080,
discovery_port=9999
)
try:
server.start()
except KeyboardInterrupt:
print("\n๐ Chiusura server...")
๐ Client Discovery (client_discovery.py)
import socket
import json
import time
from typing import List, Dict
classDiscoveryClient:
def__init__(self, discovery_port=9999, timeout=3.0):
self.discovery_port = discovery_port
self.timeout = timeout
defdiscover_services(self, max_services=10) -> List[Dict]:
"""
Scopre i servizi disponibili sulla rete locale.
Returns:
Lista di dizionari con informazioni sui servizi trovati
"""# Crea socket UDP
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
# FONDAMENTALE: Abilita broadcasting
client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
# Imposta timeout per evitare attesa infinita
client_socket.settimeout(self.timeout)
# Aumenta buffer ricezione per gestire molte risposte
client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 65536)
print("๐ Avvio discovery...")
print(f"๐ก Invio broadcast su porta {self.discovery_port}")
print(f"โฐ Timeout: {self.timeout}s\n")
# Invia messaggio broadcast
broadcast_addr = ("255.255.255.255", self.discovery_port)
client_socket.sendto(b"DISCOVER_SERVICE", broadcast_addr)
# Raccogli risposte
services = []
seen_addresses = set()
start_time = time.time()
whilelen(services) < max_services:
try:
data, server_addr = client_socket.recvfrom(4096)
# Evita duplicatiif server_addr not in seen_addresses:
seen_addresses.add(server_addr)
# Decodifica risposta JSON
service_info = json.loads(data.decode('utf-8'))
service_info['server_address'] = server_addr[0]
services.append(service_info)
print(f"โ Servizio #{len(services)} trovato!")
print(f" ๐ท๏ธ Nome: {service_info['service_name']}")
print(f" ๐ฅ๏ธ Host: {service_info['hostname']}")
print(f" ๐ IP: {service_info['server_address']}")
print(f" ๐ Porta: {service_info['service_port']}")
print(f" โฑ๏ธ Uptime: {service_info['uptime_seconds']:.0f}s\n")
except socket.timeout:
# Timeout raggiuntobreakexcept json.JSONDecodeError:
# Risposta non validacontinue
elapsed = time.time() - start_time
# Riepilogoprint(f"๐ Discovery completato in {elapsed:.2f}s")
print(f"๐ฏ Servizi trovati: {len(services)}\n")
return services
except PermissionError:
print("โ ERRORE: Permesso negato per broadcast")
print(" Verifica che SO_BROADCAST sia abilitato!")
return []
exceptExceptionas e:
print(f"โ Errore: {e}")
return []
finally:
client_socket.close()
defprint_services_table(self, services: List[Dict]):
"""Stampa una tabella formattata dei servizi"""ifnot services:
print("โ ๏ธ Nessun servizio trovato")
returnprint("โโโโโโโฌโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโฌโโโโโโโ")
print("โ # โ Servizio โ Hostname โ IP โ Portaโ")
print("โโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโผโโโโโโโค")
for idx, service inenumerate(services, 1):
print(f"โ {idx:<3} โ "f"{service['service_name']:<12} โ "f"{service['hostname']:<15} โ "f"{service['server_address']:<13} โ "f"{service['service_port']:<4} โ")
print("โโโโโโโดโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโดโโโโโโโ")
if __name__ == "__main__":
client = DiscoveryClient(
discovery_port=9999,
timeout=3.0
)
print("๐ Service Discovery Client")
print("=" * 50 + "\n")
services = client.discover_services(max_services=10)
if services:
print("\n๐ Lista Servizi Disponibili:")
client.print_services_table(services)
print("\n๐ก Per connetterti a un servizio:")
for idx, service inenumerate(services, 1):
print(f" {idx}. {service['server_address']}:{service['service_port']}")
else:
print("\n๐ Nessun servizio disponibile sulla rete")
๐ Service Discovery Client
==================================================
๐ Avvio discovery...
๐ก Invio broadcast su porta 9999
โฐ Timeout: 3.0s
โ Servizio #1 trovato!
๐ท๏ธ Nome: WebServer
๐ฅ๏ธ Host: Server-01
๐ IP: 192.168.1.10
๐ Porta: 8080
โฑ๏ธ Uptime: 3600s
โ Servizio #2 trovato!
๐ท๏ธ Nome: DatabaseServer
๐ฅ๏ธ Host: DB-Server
๐ IP: 192.168.1.20
๐ Porta: 5432
โฑ๏ธ Uptime: 7200s
๐ Discovery completato in 2.15s
๐ฏ Servizi trovati: 2
๐ Lista Servizi Disponibili:
โโโโโโโฌโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโฌโโโโโโโ
โ # โ Servizio โ Hostname โ IP โ Portaโ
โโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโผโโโโโโโค
โ 1 โ WebServer โ Server-01 โ 192.168.1.10 โ 8080 โ
โ 2 โ DatabaseServeโ DB-Server โ 192.168.1.20 โ 5432 โ
โโโโโโโดโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโดโโโโโโโ
๐ก Per connetterti a un servizio:
1. 192.168.1.10:8080
2. 192.168.1.20:5432
๐ 10. Esercizi Pratici
๐ช Esercizi per Consolidare le Competenze
Esercizio 1: Chat Room UDP con Broadcast ๐จ๏ธ
Crea un sistema di chat room dove i messaggi vengono inviati in broadcast a tutti i client sulla rete locale.
Il server deve usare SO_BROADCAST per inviare messaggi a tutti i client
Ogni client deve avere un nickname
I messaggi devono essere formattati come: [NICKNAME] messaggio
Implementa comandi /list (elenca utenti) e /quit (esci)
Esercizio 2: Server File Transfer Ottimizzato ๐
Crea un server di trasferimento file TCP che usa buffer ottimizzati per massimizzare il throughput.
Usa SO_REUSEADDR per riavvii rapidi
Configura SO_RCVBUF e SO_SNDBUF con dimensioni appropriate (almeno 1MB)
Implementa un progress bar per mostrare l'avanzamento del trasferimento
Calcola e mostra la velocitร di trasferimento in MB/s
Esercizio 3: Sistema di Monitoring con Keep-Alive ๐
Crea un sistema di monitoring dove il server tiene traccia di client connessi e rileva disconnessioni.
Usa SO_KEEPALIVE per rilevare client disconnessi
Il server deve loggare quando un client si connette/disconnette
Implementa un dashboard che mostra il numero di client attivi
Crea un semplice server di gioco multiplayer con latenza minima.
Usa TCP_NODELAY per minimizzare la latenza
Buffer piccoli (8KB) per dati sempre freschi
Implementa un sistema di coordinate dove i giocatori si muovono
Misura e mostra la latenza di ogni messaggio
Esercizio 5: Load Balancer con SO_REUSEPORT โ๏ธ
(Solo Linux) Crea un sistema dove piรน processi server ascoltano sulla stessa porta.
Usa SO_REUSEPORT per permettere multi-binding
Avvia 3-4 processi server sulla stessa porta
Il kernel distribuirร automaticamente le connessioni
Ogni server deve loggare quante richieste gestisce
๐งช Testa la Tua Comprensione!
Prova questi scenari pratici per verificare la tua comprensione
๐ 11. Conclusione
๐ฏ Cosa Hai Imparato
โ Cos'รจ setsockopt e quando usarla
โ I tre parametri fondamentali: level, optname, value
โ Broadcasting UDP con SO_BROADCAST in dettaglio
โ SO_REUSEADDR per riavvii rapidi di server
โ Gestione buffer con SO_RCVBUF e SO_SNDBUF
โ Timeout con SO_RCVTIMEO e SO_SNDTIMEO
โ Keep-alive TCP con SO_KEEPALIVE
โ Ottimizzazione latenza con TCP_NODELAY
โ Controllo chiusura con SO_LINGER
โ Best practices e pattern comuni
โ Errori comuni da evitare
โ Implementazione completa di un sistema reale
๐ Approfondimenti
Per saperne di piรน su UDP in generale, consulta:
๐ Lezione 9 - Socket UDP in Python (per i concetti base di UDP)
๐ Documentazione ufficiale Python: socket module
๐ RFC 768: User Datagram Protocol (per il protocollo UDP completo)
๐ Documentazione Linux: socket(7) man page (per dettagli su tutte le opzioni socket)
๐ก Consiglio Finale: La funzione setsockopt รจ potentissima ma richiede esperienza per
essere usata correttamente. Sperimenta con diversi valori e configurazioni nei tuoi progetti personali.
Usa sempre try-except per gestire opzioni non supportate su certi sistemi. E ricorda: quando qualcosa non
funziona come previsto, spesso รจ perchรฉ un'opzione socket non รจ configurata correttamente o รจ impostata
nell'ordine sbagliato. Buon coding! ๐โจ
Buono studio e buon coding con setsockopt! ๐ง๐